import random
import logging



logger = logging.getLogger('SSTDMAMac')



class SSTDMAMac:
    
    def __init__(self, env, node, channel, max_degree, max_clock, num_slot_clks, num_frame_slots, welcome_weight=0.5):
        self.env = env
        self.node = node
        self.channel = channel
        self.max_degree = max_degree
        self.welcome_weight = welcome_weight
        self.wait = 0
        self.wait_add = 0
        self.s = 0
        self.clk = 0
        self.fi_table = {}
        self.welcomes = {}
        self.status = 'passive'
        self.max_clock = max_clock
        self.num_slot_clks = num_slot_clks
        self.num_frame_slots = num_frame_slots
        self.last_send_clk = 0
        self.last_switch_passive_clk = 0
        self.send_control = False

    def info(self, msg):
        logger.info(f"{self.env.now:07.2f} ({self.F():02d},{self.S():02d}) {self.node.id:02d} {msg}")
    
    def debug(self, msg):
        logger.debug(f"{self.env.now:07.2f} ({self.F():02d},{self.S():02d}) {self.node.id:02d} {msg}")
    
    def slot(self, clk):
        slot = (clk // self.num_slot_clks) % self.num_frame_slots
        return slot

    def S(self):
        slot = (self.clk // self.num_slot_clks) % self.num_frame_slots
        return slot

    def F(self):
        f = self.clk // (self.num_slot_clks * self.num_frame_slots)
        return f

    def clock(self):
        return self.clk

    def advance_clock(self, x):
        self.clk = (self.clk + x) % self.max_clock

    def update_clock(self):
        while True:
            yield self.env.timeout(1)
            self.advance_clock(1)

    def used_slots(self, tab):
        used_slots = set()
        for fi in tab.values():
            used_slots.add(self.slot(fi['rxtime']))
        return used_slots

    def is_used_slot(self, slot):
        used_slots = self.used_slots(self.fi_table)
        return slot in used_slots

    def local_fis(self):
        fis = {}
        for fi in self.fi_table.values():
            if fi['occurrence'] == 'local':
                fis[fi['id']] = fi
        return fis

    def fi_table_rx_time_shift(self, x):
        for info in self.fi_table.values():
            info['rxtime'] = (info['rxtime'] + x) % self.max_clock

    def add_fis(self, fis):
        for fi in fis.values():
            if fi['occurrence'] == 'local':
                id = fi['id']
                if id not in self.fi_table:
                    self.fi_table[id] = {
                        'id': id,
                        'type': 'message',
                        'occurrence': 'remote',
                        'rxtime': self.clk,
                    }

    def backoff(self):
        r = random.randint(1, 3 * self.max_degree)
        self.wait = self.wait_add + r
        self.wait_add = 3 * self.max_degree - r
        self.debug(f"backoff {self.wait}")
    
    def start(self):
        self.node.receive = self.receive
        self.clk = random.randint(0, self.max_clock-1)
        self.last_switch_passive_clk = self.clk
        self.env.process(self.update_clock())
        self.env.process(self.main_loop())
        self.info(f"start")

    def check_conflict(self, sid, ss, fis, clk):
        sender_slot = self.slot(clk)
        if sender_slot == self.s and ss == 'active':
            self.info(f'conflict with sender:{sid} slot:{sender_slot}')
            return True
        for fi in fis.values():
            rxtime = fi['rxtime']
            slot = self.slot(rxtime)
            if self.s == slot and self.node.id != fi["id"] and fi['type'] == 'message':
                self.info(f'conflict with sender:{sid} neighbor:{fi["id"]} slot:{slot}')
                return True
        return False

    def transmit(self, FI, msg=None):
        packet = {
            "id": self.node.id,
            "clk": self.clk,
            "status": self.status,
            "FI": FI,
            "msg": msg
        }
        self.node.last_send_time = self.env.now
        self.last_send_clk = self.clk
        self.channel.transmit(self.node, packet)
        self.debug(f"send packet {packet}")
        self.send_control = True

    def receive(self, packet):
        sender_id = packet['id']
        sender_clk = packet['clk']
        sender_sts = packet['status']
        sender_fis = packet['FI']
        
        self.debug(f"recv packet from {sender_id}")

        if self.status == 'active' and self.check_conflict(sender_id, sender_sts, sender_fis, sender_clk):
            self.info(f"conflict checked, switch to passive")
            self.backoff()
            self.status = 'passive'
            self.send_control = False
            return

        if self.node.id in sender_fis and self.status == 'passive':
            self.welcomes[sender_id] = True
            local_fis = self.local_fis()
            active_thres = len(local_fis) * self.welcome_weight
            if self.check_conflict(sender_id, sender_sts, sender_fis, sender_clk) == False and \
                len(self.welcomes) >= active_thres and \
                self.send_control:
                self.welcomes = {}
                self.status = 'active'
                self.s = self.slot(sender_fis[self.node.id]['rxtime'])
                self.info(f"FIs has me, switch to active, using slot:{self.s}")

        if sender_sts == 'active':
            if 'msg' in packet:
                self.fi_table[sender_id] = {
                    'id': sender_id,
                    'type': 'message',
                    'occurrence': 'local',
                    'rxtime': self.clk,
                }
        elif sender_clk == self.clk:
            sender_slot = self.slot(sender_clk)
            if sender_id in self.fi_table:
                del self.fi_table[sender_id]
            used_slots = self.used_slots(self.fi_table)
            if sender_slot not in used_slots:
                self.info(f'ack to {sender_id}')
                self.fi_table[sender_id] = {
                    'id': sender_id,
                    'type': 'welcome',
                    'occurrence': 'local',
                    'rxtime': self.clk,
                }
                
        if self.clk < sender_clk:
            self.info(f"sync clk to {sender_clk}")
            self.advance_clock(sender_clk - self.clk)
            self.fi_table_rx_time_shift(sender_clk - self.clk)
            if self.status == 'active':
                self.status = 'passive'
                self.send_control = False
                self.backoff()
                self.info("switch to passive")

        if self.clk == sender_clk:
            self.add_fis(sender_fis)

    def active_process(self, s):
        # Active node backoff count
        if s == self.s:
            self.transmit(self.local_fis())

    def passive_process(self, s):
        if self.is_used_slot(s)==False:
            self.wait = max(0, self.wait-1)
            if self.wait == 0:
                self.s = self.S()
                self.transmit(self.local_fis())
                self.backoff()

    def main_loop(self):
        while True:
            yield self.env.timeout(1)
            if self.clk % self.num_slot_clks == 0:
                s = self.slot(self.clk)
                if self.status == 'active':
                    self.active_process(s)
                else:
                    self.passive_process(s)